/*!
 * \file type_datetime.c
 * \author Fabien Arcellier (fabien_arcellier@yahoo.fr)
 * \version 1.0
 * 
 * This file contains implementation to encode and manipulate date and time through DATETIME type.
 */

#include <assert.h>
#include <stdlib.h>
#include "../debug.h"
#include "../../includes/type_datetime.h"

/*! \brief Create a new instance of an object DATETIME.
 *  \return 0 if success, otherwise an error code (type DATETIME_ERROR)
 * 
 * All Fields are initiate at zero.
 */
int DATETIME_Create(PTR_DATETIME *ptrDateTime)
{
  // Pre conditions
  assert(*ptrDateTime == NULL);
  
  // Actions
  *ptrDateTime = (PTR_DATETIME) malloc(sizeof(DATETIME));
  if (ptrDateTime == NULL)
  {
    return DATETIME_ERROR_1_MALLOC_INIT_FAILED;
  }
  
  (*ptrDateTime) -> year = 0;
  (*ptrDateTime) -> month = 1;
  (*ptrDateTime) -> day = 1;
  (*ptrDateTime) -> hour = 0;
  (*ptrDateTime) -> minute = 0;
  (*ptrDateTime) -> second = 0;
  (*ptrDateTime) -> msecond = 0;
  
  // Post conditions
  assert(DATETIME_CheckInvariant(*ptrDateTime));
  
  return DATETIME_OPERATION_SUCCESS;
}

/*! \brief Destroy an instance of an object DATETIME
 *  \return 0 if success, otherwise an error code
 * 
 * ptrDateTime is set at NULL
 */
int DATETIME_Destroy(PTR_DATETIME *ptrDateTime)
{
  // Pre conditions
  assert(DATETIME_CheckInvariant(*ptrDateTime));
  
  free(*ptrDateTime);
  *ptrDateTime = NULL;
  
  // Post conditions
  assert(*ptrDateTime == NULL);
  
  return DATETIME_OPERATION_SUCCESS;
}

/*! \brief Compare two instances of DateTime
 * \return 1 if ptrDateTime1 is after ptrDateTime2, 0 if ptrDateTime1 == ptrDateTime2, otherwise -1
 */
int DATETIME_Compare(PTR_DATETIME ptrDateTime1, PTR_DATETIME ptrDateTime2)
{
  // Pre conditions
  assert(DATETIME_CheckInvariant(ptrDateTime1));
  assert(DATETIME_CheckInvariant(ptrDateTime2));
  
  // Actions
  int result_comparaison = -2;
  if (ptrDateTime1 -> year < ptrDateTime2 -> year)
  {
    result_comparaison = -1;
  }
  else if(ptrDateTime1 -> year > ptrDateTime2 -> year)
  {
    result_comparaison = 1;
  }
  else if(ptrDateTime1 -> month < ptrDateTime2 -> month)
  {
    result_comparaison = -1;
  }
  else if(ptrDateTime1 -> month > ptrDateTime2 -> month)
  {
    result_comparaison = 1;
  }
  else if(ptrDateTime1 -> day < ptrDateTime2 -> day)
  {
    result_comparaison = -1;
  }
  else if(ptrDateTime1 -> day > ptrDateTime2 -> day)
  {
    result_comparaison = 1;
  }
  else if(ptrDateTime1 -> hour < ptrDateTime2 -> hour)
  {
    result_comparaison = -1;
  }
  else if(ptrDateTime1 -> hour > ptrDateTime2 -> hour)
  {
    result_comparaison = 1;
  }
  else if(ptrDateTime1 -> minute < ptrDateTime2 -> minute)
  {
    result_comparaison = -1;
  }
  else if(ptrDateTime1 -> minute > ptrDateTime2 -> minute)
  {
    result_comparaison = 1;
  }
  else if(ptrDateTime1 -> second < ptrDateTime2 -> second)
  {
    result_comparaison = -1;
  }
  else if(ptrDateTime1 -> second > ptrDateTime2 -> second)
  {
    result_comparaison = 1;
  }
  else if(ptrDateTime1 -> msecond < ptrDateTime2 -> msecond)
  {
    result_comparaison = -1;
  }
  else if(ptrDateTime1 -> msecond > ptrDateTime2 -> msecond)
  {
    result_comparaison = 1;
  }
  else
  {
    result_comparaison = 0;
  }

  // Post conditions
  assert(DATETIME_CheckInvariant(ptrDateTime1));
  assert(DATETIME_CheckInvariant(ptrDateTime2));
  assert(result_comparaison == 0 || result_comparaison == -1 || result_comparaison == 1);
  
  return result_comparaison;
}

/*! \brief Get date information from an instance of DATETIME
 *  \return 0 if success, otherwise an error code
 */
int DATETIME_GetDate(PTR_DATETIME ptrDateTime, int *year, int *month, int *day)
{
  // Précondition
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  //Actions
  *year = ptrDateTime -> year;
  *month = ptrDateTime -> month;
  *day = ptrDateTime -> day;
  
  // Post condition
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  return DATETIME_OPERATION_SUCCESS;
}

/*! \brief Set date information to an instance of DATETIME
 *  \return 0 if success, otherwise an error code
 * 
 * Preconditions
 * - The date must be valid if not, when ndebug == 1, the date won't be modified otherwise, assert will
 *  trigg. Use DATETIME_IsDateValid to valid your date.
 */
int DATETIME_SetDate(PTR_DATETIME ptrDateTime, int year, int month, int day)
{
  // Précondition
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  // Actions
  if (DATETIME_IsDateValid(year, month, day) == 1)
  {
    ptrDateTime -> year = year;
    ptrDateTime -> month = month;
    ptrDateTime -> day = day;
  }
  
  // Post condition
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  return DATETIME_OPERATION_SUCCESS;
}

/*! \brief Add one or many days to an instance of a DateTime
 * \return 0 if success otherwise error code
 * 
 * If nbDay is negative, the instance of datetime go to the past.
 */
int DATETIME_AddDay(PTR_DATETIME ptrDateTime, int nbDay)
{
  // Preconditions
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  // Actions
  int day_to_add = nbDay;
  while(day_to_add != 0)
  {
    int nb_day_in_month = DATETIME_GetDaysInMonth(ptrDateTime -> year, ptrDateTime -> month);
    if (ptrDateTime -> day + day_to_add > nb_day_in_month)
    {
      day_to_add = day_to_add - (nb_day_in_month + 1 - ptrDateTime -> day);
      DATETIME_AddMonth(ptrDateTime, 1);
      ptrDateTime -> day = 1;
    }
    else if (ptrDateTime -> day + day_to_add < 1)
    {
      day_to_add = day_to_add + ptrDateTime -> day;
      DATETIME_AddMonth(ptrDateTime, -1);
      ptrDateTime -> day = DATETIME_GetDaysInMonth(ptrDateTime -> year, ptrDateTime -> month);
    }
    else
    {
      ptrDateTime -> day = ptrDateTime -> day + day_to_add;
      day_to_add = 0;
    }
  }
  
  // Post conditions
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  return DATETIME_OPERATION_SUCCESS;
}

/*! \brief Add one or many months to an instance of a DateTime
 * \return 0 if success otherwise error code
 * 
 * If nbMonth is negative, the instance of datetime go to the past.
 */
int DATETIME_AddMonth(PTR_DATETIME ptrDateTime, int nbMonth)
{
  // Preconditions
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  // Actions
  int month_to_add = nbMonth;
  while(month_to_add != 0)
  {
    if (ptrDateTime -> month + month_to_add > 12)
    {
      month_to_add = month_to_add - (12 + 1 - ptrDateTime -> month);
      DATETIME_AddYear(ptrDateTime, 1);
      ptrDateTime -> month = 1;
    }
    else if(ptrDateTime -> month + month_to_add < 1)
    {
      month_to_add = month_to_add + (ptrDateTime -> month);
      DATETIME_AddYear(ptrDateTime, -1);
      ptrDateTime -> month = 12;
    }
    else
    {
      ptrDateTime -> month = ptrDateTime -> month + month_to_add;
      month_to_add = 0;
    }
  }
    
  // Post conditions
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  return DATETIME_OPERATION_SUCCESS;
}

/*! \brief Add one or many year to an instance of a DateTime
 * \return 0 if success otherwise error code
 * 
 * If nbYear is negative, the instance of datetime go to the past.
 */
int DATETIME_AddYear(PTR_DATETIME ptrDateTime, int nbYear)
{
  // Preconditions
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  
  // Actions
  ptrDateTime -> year = ptrDateTime -> year + nbYear;
    
  // Post conditions
  assert(DATETIME_CheckInvariant(ptrDateTime));
  
  return DATETIME_OPERATION_SUCCESS;
}

/*! \brief Verify a date is valid
 *  \return 0 if valid, otherwise 1
 * 
 * Check :
 * - Month value is between 1 and 12
 * - Day value is between 0 and 31
 * - Day value is valid in relation with Month and Date (if Month has 28, 29 or 30 days)
 */
int DATETIME_IsDateValid(int year, int month, int day)
{
  //Actions
  int is_valid_date = 1;
  if (month < 1 || month > 12)
  {
    is_valid_date = 0;
  }
  else if(day < 1 || day > DATETIME_GetDaysInMonth(year, month))
  {
    is_valid_date = 0;
  }
  
  // Post conditions
  assert(is_valid_date == 0 || is_valid_date == 1);
  
  return is_valid_date;
}

/*! \brief Verify the integrity of a DateTime structure
 * 
 * Invariant :
 * - ptrDateTime != NULL
 * - ptrDateTime is a valid date
 */
int DATETIME_CheckInvariant(PTR_DATETIME ptrDateTime)
{
  if (ptrDateTime != NULL ||
    DATETIME_IsDateValid(ptrDateTime -> year, ptrDateTime -> month, ptrDateTime -> day) == 1)
  {
    return 1;
  }
  else
  {
    return 0;
  }
}

/*! \brief Get the number of day in the month specified
 * \return the number of day, that is to say 28, 29, 30 or 31
 */
int DATETIME_GetDaysInMonth(int year, int month)
{
  // Pré conditions
  assert(month >= 1 || month <= 12);
  
  // Actions
  int nb_day_month[12];
  int nb_day = 0;
  nb_day_month[0] = 31;
  nb_day_month[1] = ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) ? 29 : 28;
  nb_day_month[2] = 31;
  nb_day_month[3] = 30;
  nb_day_month[4] = 31;
  nb_day_month[5] = 30;
  nb_day_month[6] = 31;
  nb_day_month[7] = 31;
  nb_day_month[8] = 30;
  nb_day_month[9] = 31;
  nb_day_month[10] = 30;
  nb_day_month[11] = 31;
  
  nb_day = nb_day_month[month - 1];
  
  // Post conditions
  assert(nb_day >= 28 && nb_day <= 31);
  
  return nb_day;
}
